package com.magicyuan.search.controller;

import java.io.IOException;
import java.lang.reflect.Field;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.RangeQueryBuilder;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.AggregationBuilder;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.range.Range;
import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.rescore.QueryRescorerBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.alibaba.fastjson.JSONObject;
import com.magicyuan.search.restful.pojo.DeepQueryReqInfo;
import com.magicyuan.search.restful.pojo.ESResponeObj;
import com.magicyuan.search.restful.pojo.FilterInfo;
import com.magicyuan.search.restful.pojo.SearchDetail;
import com.magicyuan.search.restful.pojo.SearchResult;
import com.magicyuan.search.utils.PropertiesUtils;
import com.magicyuan.search.utils.SearchClientHelper;

/**
 * ES搜索API<br>
 * 简单封装，供外部系统调用
 * 
 * @author yuanxiaobin
 * @see
 * @since
 */
@Controller
@RequestMapping("/searchEngines")
public class ElasticSearchController {

	private static Logger logger = LoggerFactory.getLogger(ElasticSearchController.class);

	/**
	 * 基本检索服务<br>
	 * 输入输出均为json对象 请求POST方式，json内容在body体内
	 * 
	 * @param queryReqInfo
	 * @return ESResponeObj
	 * @throws IOException
	 */
	@RequestMapping(value = "/productSearch", method = RequestMethod.POST, consumes = "application/json")
	@ResponseBody
	public ESResponeObj getSearchResult(@RequestBody DeepQueryReqInfo queryReqInfo) throws IOException {

		logger.debug(JSONObject.toJSONString(queryReqInfo));
		int from = 0;
		int pageNum = 1;
		int pageSize = 10;

		// 处理分页信息
		if (StringUtils.isNotBlank(queryReqInfo.getPageNumber())) {

			pageNum = Integer.parseInt(queryReqInfo.getPageNumber());
		}

		if (StringUtils.isNotBlank(queryReqInfo.getPageSize())) {

			pageSize = Integer.parseInt(queryReqInfo.getPageSize());
		}

		from = (pageNum - 1) * pageSize;

		List<FilterInfo> filterList = queryReqInfo.getFilterList();

		// 获取字段权重配置信息
		String matchQueryConfig = PropertiesUtils.getValue("matchQueryField");

		// 获取字段是否存在的打分权重配置信息
		String existsQueryConfig = PropertiesUtils.getValue("existsQueryField");

		// 获取区间范围的字段打分权重配置信息
		String rangeQueryConfig = PropertiesUtils.getValue("rangeQueryField");

		// 获取搜索的词组的匹配宽松度，数值越大命中文档数量越多
		String matchPhraseSlop = PropertiesUtils.getValue("matchPhraseSlop");

		// 建立ES客户端连接，指定搜索index和type以及分页信息
		SearchRequestBuilder requestBuilder = SearchClientHelper.getInstance().getClient().prepareSearch("product")
				.setTypes("orf").setFrom(from).setSize(pageSize);

		// 设置boolQuery的基本查询条件，默认全字段匹配
		BoolQueryBuilder boolqb = QueryBuilders.boolQuery().must(QueryBuilders
				.matchPhraseQuery("_all", queryReqInfo.getSearchkey()).slop(Integer.parseInt(matchPhraseSlop)));

		boolqb = configMatchFieldScore(matchQueryConfig, boolqb);

		boolqb = configExistsFieldScore(existsQueryConfig, boolqb);

		boolqb = configRangeFieldScore(rangeQueryConfig, boolqb);

		// 处理前台传入的过滤条件
		processFilterInfo(filterList, boolqb);

		// 处理配置的衰减函数信息
		configDecayFunctionFieldScore(boolqb);

		// 设置bool查询对象到请求的对象里面去
		requestBuilder = requestBuilder.setQuery(boolqb);

		configRescoreField(queryReqInfo, requestBuilder);

		// 获取配置文件中的普通值聚合信息
		String aggregationConfig = PropertiesUtils.getValue("aggregationField");
		String[] aggregationFields = configTermsAggregation(requestBuilder, aggregationConfig);

		// 获取配置文件中的范围聚合信息聚合信息
		String aggregationRangeConfig = PropertiesUtils.getValue("aggregationRangeField");
		String[] aggregationRangeFields = configRangeAggregation(requestBuilder, aggregationRangeConfig);

		// 执行搜索操作，获取ES的响应对象
		SearchResponse response = requestBuilder.setExplain(true).execute().actionGet();

		// 定义封装之后的返回对象
		ESResponeObj esRespObj = new ESResponeObj();

		// 定义结果的聚合数据集合
		Map<String, Map<String, Object>> aggregationsMap = new LinkedHashMap<String, Map<String, Object>>();

		processTermsAggregationResult(aggregationFields, response, aggregationsMap);

		processRangeAggregationResult(aggregationRangeFields, response, aggregationsMap);

		// 解析并封装ES搜索结果中的文档内容 针对ORF和产品做处理
		List<SearchResult> resultList = new ArrayList<SearchResult>();
		if (!ArrayUtils.isEmpty(response.getHits().getHits())) {

			// 调试模式输出对象中包含打分的说明信息
			String isDebug = PropertiesUtils.getValue("isDebug");
			// 获取搜索结果中的内容结果集
			SearchHit[] searchHits = response.getHits().getHits();

			for (SearchHit temp : searchHits) {
				SearchResult result = new SearchResult();
				result.setType(temp.getType());
				result.setId(temp.getId());
				result.setIndex(temp.getIndex());

				if (Boolean.parseBoolean(isDebug)) {
					result.setExplanation(temp.getExplanation());
				}
				result.setResult(temp.getSource());
				result.setScore(temp.getScore());
				resultList.add(result);
			}

		}

		// 设置返回对象的相关信息，聚合对象 总记录数以及结果集
		esRespObj.setAggregations(aggregationsMap);
		esRespObj.setTotalHits(response.getHits().getTotalHits());
		esRespObj.setResultList(resultList);

		return esRespObj;
	}

	/**
	 * 处理配置文件中的衰减函数字段信息<br>
	 * 〈功能详细描述〉
	 * 
	 * @param boolqb
	 * @since
	 */
	private void configDecayFunctionFieldScore(BoolQueryBuilder boolqb) {
		// 获取衰减函数的配置信息，目前仅针对价格进行衰减
		String linearDecayConfig = PropertiesUtils.getValue("linearDecayField");

		// 获取衰减原点的配置值，即抛物线的最高点X值
		String linearOrigin = PropertiesUtils.getValue(linearDecayConfig + "Origin");

		// 获取衰减的幅度
		String linearScale = PropertiesUtils.getValue(linearDecayConfig + "Scale");

		// 获取衰减函数的总分，值为0-1之间，不包括1 即抛物线的最高点Y值
		String linearDecay = PropertiesUtils.getValue(linearDecayConfig + "Decay");

		// 设置搜索的衰减函数对象 当前为线性衰减函数
		FunctionScoreQueryBuilder query = QueryBuilders.functionScoreQuery(
				ScoreFunctionBuilders.linearDecayFunction(linearDecayConfig, Float.parseFloat(linearOrigin),
						Float.parseFloat(linearScale), 0, Float.parseFloat(linearDecay)));

		// 把衰减函数加到搜索里面去，should为或者条件 must为且
		boolqb.should(query);
	}

	/**
	 * 处理ES返回的检索结果中的范围聚合信息<br>
	 * 〈功能详细描述〉
	 * 
	 * @param aggregationRangeFields
	 * @param response
	 * @param aggregationsMap
	 */
	private void processRangeAggregationResult(String[] aggregationRangeFields, SearchResponse response,
			Map<String, Map<String, Object>> aggregationsMap) {
		// 分析结果中的范围聚合数据
		for (String aggregField : aggregationRangeFields) {
			Range rangeAggInfo = response.getAggregations().get(aggregField);
			Map<String, Object> rangeInfoMap = new HashMap<String, Object>();
			for (Range.Bucket entry : rangeAggInfo.getBuckets()) {

				// 解析出数据中的from和to返回到前台
				Map<String, String> rangeEntryMap = new HashMap<String, String>();
				rangeEntryMap.put("count", String.valueOf(entry.getDocCount()));
				rangeEntryMap.put("from", entry.getFromAsString());
				rangeEntryMap.put("to", entry.getToAsString());
				rangeInfoMap.put(entry.getKey().toString(), rangeEntryMap);
			}

			aggregationsMap.put(aggregField, rangeInfoMap);
		}
	}

	/**
	 * 处理ES返回的检索结果中的值匹配聚合信息<br>
	 * 〈功能详细描述〉
	 * 
	 * @param aggregationFields
	 * @param response
	 * @param aggregationsMap
	 * @since
	 */
	private void processTermsAggregationResult(String[] aggregationFields, SearchResponse response,
			Map<String, Map<String, Object>> aggregationsMap) {
		// 分析结果中的聚合数据
		for (String aggregField : aggregationFields) {

			// 根据值匹配的聚合配置字段，获取结果中的值匹配聚合内容
			Terms termsAggInfo = response.getAggregations().get(aggregField);

			// 定义返回前台结果中的聚合对象，不改变ES返回的聚合顺序使用LinkedHashMap
			Map<String, Object> termsAggInfoMap = new LinkedHashMap<String, Object>();
			for (Terms.Bucket entry : termsAggInfo.getBuckets()) {

				// 组装聚合数据中的字段值和文档数量
				termsAggInfoMap.put(entry.getKey().toString(), String.valueOf(entry.getDocCount()));
			}

			aggregationsMap.put(aggregField, termsAggInfoMap);
		}
	}

	/**
	 * 处理配置文件中的范围聚合统计的字段信息 并返回分割后的字段数组<br>
	 * 〈功能详细描述〉
	 * 
	 * @param requestBuilder
	 * @param aggregationRangeConfig
	 * @since
	 */
	private String[] configRangeAggregation(SearchRequestBuilder requestBuilder, String aggregationRangeConfig) {
		String[] aggregationRangeFields = aggregationRangeConfig.split(",");

		for (String rangeField : aggregationRangeFields) {

			String rangeInfo = PropertiesUtils.getValue(rangeField + "Range");

			RangeAggregationBuilder rangeAggregation = AggregationBuilders.range(rangeField).field(rangeField);

			// 分割相应字段范围信息
			String[] ranges = rangeInfo.split(",");

			for (int i = 0; i < ranges.length; i++) {

				// 第一位为范围的最小值一直到开始值
				if (i == 0) {
					rangeAggregation.addUnboundedTo(Float.parseFloat(ranges[i]));
				} else if (i == ranges.length - 1) { // 最后一位为范围的结束值到无穷大
					rangeAggregation.addUnboundedFrom(Float.parseFloat(ranges[i]));
				} else {

					// 不是开始和结束的则分割- 解析出from和to的值
					float rangeFrom = Float.parseFloat(ranges[i].split("-")[0]);
					float rangeTo = Float.parseFloat(ranges[i].split("-")[1]);

					// 设置范围的开始和结束
					rangeAggregation.addRange(rangeFrom, rangeTo);
				}

			}

			requestBuilder.addAggregation(rangeAggregation);
		}
		return aggregationRangeFields;
	}

	/**
	 * 处理配置文件的值匹配的聚合统计信息 并返回分割之后的聚合字段数组<br>
	 * 〈功能详细描述〉
	 * 
	 * @param requestBuilder
	 * @param aggregationConfig
	 * @since
	 */
	private String[] configTermsAggregation(SearchRequestBuilder requestBuilder, String aggregationConfig) {
		// 分割值聚合信息
		String[] aggregationFields = aggregationConfig.split(",");

		// 遍历配置信息，创建聚合对象，加入到请求对象中 terms聚合
		for (String aggregField : aggregationFields) {

			// 设置值聚合条件，指定返回统计结果最大30条，并且按照统计出的文档数量进行排序
			AggregationBuilder aggregation = AggregationBuilders.terms(aggregField).field(aggregField).size(30)
					.order(Terms.Order.count(false));
			requestBuilder.addAggregation(aggregation);
		}
		return aggregationFields;
	}

	/**
	 * 处理二次打分的配置信息<br>
	 * 〈功能详细描述〉
	 * 
	 * @param queryReqInfo
	 * @param requestBuilder
	 * @see
	 * @since
	 */
	private void configRescoreField(DeepQueryReqInfo queryReqInfo, SearchRequestBuilder requestBuilder) {
		// 设置二次打分配置信息
		String rescoreConfig = PropertiesUtils.getValue("reScoreField");
		if (StringUtils.isNotBlank(rescoreConfig)) {
			String[] rescoreFields = rescoreConfig.split(",");
			for (String rescoreField : rescoreFields) {
				String rescoreFieldValue = PropertiesUtils.getValue(rescoreField + "Value");
				if (StringUtils.isBlank(rescoreFieldValue)) {
					QueryRescorerBuilder qrb = QueryRescorerBuilder
							.queryRescorer(QueryBuilders.matchQuery(rescoreField, queryReqInfo.getSearchkey()));
					qrb.setQueryWeight(Float.parseFloat(PropertiesUtils.getValue(rescoreField + "QW")));
					qrb.setRescoreQueryWeight(Float.parseFloat(PropertiesUtils.getValue(rescoreField + "RQW")));
					requestBuilder.setRescorer(qrb);
				} else {

					String[] rescoreValues = rescoreFieldValue.split(",");
					String qwConfigValue = PropertiesUtils.getValue(rescoreField + "QW");
					String rqwConfigValue = PropertiesUtils.getValue(rescoreField + "RQW");
					String[] qwValues = qwConfigValue.split(",");
					String[] rqwValues = rqwConfigValue.split(",");

					if (rescoreValues.length > 1) {

						for (int i = 0; i < rescoreValues.length; i++) {
							QueryRescorerBuilder qrb = QueryRescorerBuilder
									.queryRescorer(QueryBuilders.matchQuery(rescoreField, rescoreValues[i]));
							qrb.setQueryWeight(Float.parseFloat(qwValues[i]));
							qrb.setRescoreQueryWeight(Float.parseFloat(rqwValues[i]));
							requestBuilder.setRescorer(qrb);
						}

					} else {
						QueryRescorerBuilder qrb = QueryRescorerBuilder
								.queryRescorer(QueryBuilders.matchQuery(rescoreField, rescoreFieldValue));
						qrb.setQueryWeight(Float.parseFloat(PropertiesUtils.getValue(rescoreField + "QW")));
						qrb.setRescoreQueryWeight(Float.parseFloat(PropertiesUtils.getValue(rescoreField + "RQW")));
						requestBuilder.setRescorer(qrb);
					}
				}

			}
		}
	}

	/**
	 * 处理前台传入的过滤条件<br>
	 * 〈功能详细描述〉
	 * 
	 * @param filterList
	 * @param boolqb
	 * @see
	 * @since
	 */
	private void processFilterInfo(List<FilterInfo> filterList, BoolQueryBuilder boolqb) {
		// 分析前台传入过滤条件
		if (null != filterList) {

			// 定义普通值匹配的字段集合
			Map<String, List<String>> termsFilterMap = new HashMap<String, List<String>>();
			for (FilterInfo filterInfo : filterList) {

				// 如果不是范围过滤的则存入集合中，用于后续过滤时针对同一字段进行或者的操作
				if (StringUtils.isBlank(filterInfo.getRangeFrom()) && StringUtils.isBlank(filterInfo.getRangeTo())) {

					// 如果Map中存在了相应字段则在List中add，如果不存在则初始化一个list集合
					List<String> valueList = termsFilterMap.get(filterInfo.getFieldName());
					if (CollectionUtils.isEmpty(valueList)) {
						List<String> tempFieldValueList = new ArrayList<String>();
						tempFieldValueList.add(filterInfo.getFieldValue());
						termsFilterMap.put(filterInfo.getFieldName(), tempFieldValueList);
					} else {
						termsFilterMap.get(filterInfo.getFieldName()).add(filterInfo.getFieldValue());
					}
				}
			}

			// 处理范围的过滤，目前范围过滤加入到搜索条件进行查询
			for (FilterInfo filterInfo : filterList) {

				if (StringUtils.isNotBlank(filterInfo.getRangeFrom())
						|| StringUtils.isNotBlank(filterInfo.getRangeTo())) {
					RangeQueryBuilder rangeqb = QueryBuilders.rangeQuery(filterInfo.getFieldName());

					if (StringUtils.isNotBlank(filterInfo.getRangeFrom())) {
						rangeqb.from(Float.parseFloat(filterInfo.getRangeFrom()));
					}

					if (StringUtils.isNotBlank(filterInfo.getRangeTo())) {
						rangeqb.to(Float.parseFloat(filterInfo.getRangeTo()));
					}

					boolqb.should(rangeqb);
				}

			}

			// 处理值匹配的过滤，使用ES的filter进行过滤 同一字段名的或操作 ，不同字段的且操作
			if (MapUtils.isNotEmpty(termsFilterMap)) {
				Set<Entry<String, List<String>>> entrySet = termsFilterMap.entrySet();
				for (Entry<String, List<String>> entry : entrySet) {
					boolqb.filter(QueryBuilders.termsQuery(entry.getKey(), entry.getValue()));
				}

			}

		}
	}

	/**
	 * 处理配置文件中的范围打分信息<br>
	 * 
	 * @param rangeQueryConfig
	 * @param boolqb
	 * @since
	 */
	private BoolQueryBuilder configRangeFieldScore(String rangeQueryConfig, BoolQueryBuilder boolqb) {
		// 设置范围字段打分权重
		if (StringUtils.isNotBlank(rangeQueryConfig)) {
			String[] rangeQueryFields = rangeQueryConfig.split(",");
			for (String tempRangeField : rangeQueryFields) {

				RangeQueryBuilder rangeBulider = QueryBuilders.rangeQuery(tempRangeField);

				String rangefrom = PropertiesUtils.getValue(tempRangeField + "RangeFrom");
				String rangeTo = PropertiesUtils.getValue(tempRangeField + "RangeTo");
				String rangeBoost = PropertiesUtils.getValue(tempRangeField + "RangeBoost");

				if (StringUtils.isNotBlank(rangefrom)) {
					rangeBulider.from(rangefrom);
				}

				if (StringUtils.isNotBlank(rangeTo)) {
					rangeBulider.to(rangeTo);
				}

				// 设置搜索的打分信息，如指定字段值在相应范围则按照配置的系数给分
				boolqb = boolqb.should(rangeBulider.boost(Float.parseFloat(rangeBoost)));
			}
		}
		return boolqb;
	}

	/**
	 * 处理配置文件中的字段是否存在打分信息<br>
	 * 
	 * @param existsQueryConfig
	 * @param boolqb
	 * @since
	 */
	private BoolQueryBuilder configExistsFieldScore(String existsQueryConfig, BoolQueryBuilder boolqb) {
		// 设置字段是否存在打分权重 所谓不存在即空值，类似数据库中的null
		if (StringUtils.isNotBlank(existsQueryConfig)) {

			// 分割配置字段
			String[] existsQueryFields = existsQueryConfig.split(",");

			// 设置对应字段分值系数
			String existsBoost = PropertiesUtils.getValue("existsBoost");
			String[] existsBoosts = existsBoost.split(",");

			for (int i = 0; i < existsQueryFields.length; i++) {

				// 设置搜索的打分信息，如指定字段有值则按照配置的系数给分
				boolqb = boolqb.should(
						QueryBuilders.existsQuery(existsQueryFields[i]).boost(Float.parseFloat(existsBoosts[i])));
			}
		}
		return boolqb;
	}

	/**
	 * 处理配置文件中的值匹配打分权重<br>
	 * 根据相应字段匹配的值进行打分
	 * 
	 * @param matchQueryConfig
	 *            配置信息
	 * @param boolqb
	 *            查询对象
	 * @return
	 * @since
	 */
	private BoolQueryBuilder configMatchFieldScore(String matchQueryConfig, BoolQueryBuilder boolqb) {
		// 设置字段打分权重
		if (StringUtils.isNotBlank(matchQueryConfig)) {

			// 分割配置文件中的配置字段名
			String[] matchQueryFields = matchQueryConfig.split(",");

			// 遍历字段名，并分获取匹配值和相应分数系数
			for (String tempFields : matchQueryFields) {

				// 获取匹配值
				String matchValue = PropertiesUtils.getValue(tempFields + "matchValue");

				// 获取匹配分数系数
				String matchBoost = PropertiesUtils.getValue(tempFields + "matchBoost");

				String[] matchValues = matchValue.split(",");
				String[] matchBoosts = matchBoost.split(",");

				for (int i = 0; i < matchValues.length; i++) {
					// 设置搜索的打分信息，如果匹配上指定字段的指定值则按照配置的系数给分
					boolqb = boolqb.should(QueryBuilders.matchQuery(tempFields, matchValues[i])
							.boost(Float.parseFloat(matchBoosts[i])));
				}
			}
		}
		return boolqb;
	}

	/**
	 * 详细信息查询服务<br>
	 * 根据指定index type 和文档id获取具体文档的详细信息
	 * 
	 * @param searchDetail
	 * @return Map<String, Object> 具体文档对象的 json数据
	 * @throws UnknownHostException
	 * @since
	 */
	@RequestMapping(value = "/productDetail", method = RequestMethod.POST, consumes = "application/json")
	@ResponseBody
	public Map<String, Object> getDetail(@RequestBody SearchDetail searchDetail) throws UnknownHostException {
		GetResponse getResp = SearchClientHelper.getInstance().getClient()
				.get(new GetRequest(searchDetail.getIndex(), searchDetail.getType(), searchDetail.getId())).actionGet();
		return getResp.getSource();
	}
}
